模块是C++ 20的四大功能之一:概念,范围,协程和模块。模块还是有很多前景:编译时改进了宏隔离,废除头文件等极其不优美的解决方法。我会通过一段浅显易懂的代码带你解析模块的优势,考虑到大家普遍对C++11比较熟悉,示例中的代码无特殊说明默认是C++11,因为11大家都懂,所以11的代码方便暴露出问题,一上手就是20可能对其优势就无感啦。
执行一段大家都懂的Demo
毫无疑问,当然得从“Hello World”开始了!
// helloWorld.cpp #include <iostream> int main() { std::cout << "Hello World" << std::endl; }
编译一下,我们来创建可执行文件helloWorld,屏幕截图中的100和12928代表字节数,如下图所示。
编译
经典的构建过程
我们都知道经典的构建过程包括三个步骤:预处理,编译和链接。下面就分别详细展开介绍一下吧!
预处理
预处理器处理预处理器指令“#include”和“#define”。预处理程序将#inlude指令替换为相应的头文件,并替换宏(#define)。当然有些指令,例如#if,#else,#elif,#ifdef,#ifndef,和#endif中的部分可以被替换排除。
我们通过使用GCC上的-E或Windows上/E的编译器参数,可以观察到源码的替换过程。
预处理步骤的输出超过50万字节。所以你就不要责怪GCC慢了,因为其他编译器也很冗长,当然你可以用CompilerExplorer实际测试一下。
预处理器的输出是编译器的输入,那么我们接下来看看代码展开后是如何编译成汇编代码的?
汇编
编译是在预处理器的每个输出上单独执行的,编译器解析C++源代码并将其转换为汇编代码。生成的文件称为目标文件,是二进制形式的已编译代码。目标文件可以引用没有定义的符号。可以将目标文件归档到文件中,以供以后复用,我们称这些存档为静态库。
编译器生成的对象或转换单元是链接器的输入,接下来我们看看链接器。
链接器
链接器的输出可以是可执行文件,也可以是静态或共享库。链接程序的工作是将对引用的引用解析为未定义的符号,这些符号需要在目标文件或库中定义。
以上三步的构建过程是从C继承过来的,当然C++还有翻译单元。我们的程序由一个或多个翻译单元组成,当同一名称在不同的翻译单元中有两个不同的定义时,将发生链接器错误,这个问题在20中已经解决啦,且听下解!
构建过程中存在的问题
在经典构建过程中可能会遇到如下一些缺陷。在C++20 中,模块被引入的替代方法,是可以完全克服这些问题。
预处理器重复处理问题
首先预处理程序将#inlude指令替换为相应的头文件。我来更改一下上面的helloWorld.cpp程序以使得头重复定义出来。我重构了程序,并添加了两个源文件hello.cpp和world.cpp。源文件hello.cpp提供功能“hello”,源文件world.cpp提供功能“world”。两个源文件都包含相应的头。重构意味着该程序执行与先前程序helloWorld.cpp相同的操作。 虽然分开重构了,但是内部结构发生了变化,如下:
hello.cpp和hello.h // hello.cpp #include "hello.h" void hello() { std::cout << "hello "; } // hello.h #include <iostream> void hello(); world.cpp和world.h // world.cpp #include "world.h"
void world() { std::cout << "world"; } // world.h #include <iostream> void world(); helloWorld2.cpp // helloWorld2.cpp #include <iostream> #include "hello.h" #include "world.h" int main() { hello(); world(); std::cout << std::endl; }
构建和执行程序:
编译
这里就有一个问题了:预处理程序在每个源文件上运行,意味着头文件
编译出3次
显然,重复预处理的做法会增加编译时间。不过在C++20中你只需要导入模块一次,根本就不会造成重复预处理操作。
与预处理器宏的隔离问题
根据C++发展的一个方向,那就是应该摆脱预处理器宏。为什么呢?使用宏只是文本替换,不包括任何C ++语义。预处理器宏带来了很多麻烦,例如包含宏的顺序问题,宏名称冲突等。
有如下webcolors.h和productinfo.h:
// webcolors.h #define RED 0xFF0000 // productinfo.h #define RED 0
当源文件client.cpp包含这两个标头时,宏RED的值取决于标头包含的顺序,所以有时会出错。但是,导入模块的顺序是没有区别。
符号的多重定义
ODR代表“定义规则”,其功能如下:
一个功能在任何翻译单元中的定义不得超过一个。
一个函数在程序中的定义不能超过一个。
具有外部链接的内联函数可以在多个翻译中进行定义。定义必须满足每个定义必须相同的要求。
让我们看看,当我来违反上面一个定义规则时,看看链接器输出是什么。以下代码示例包含两个头文件header.h和header2.h。主程序包含两次头文件header.h,所以就打破了第一个定义规则,因为其中包括了func的两个定义。
// header.h void func() {} // header2.h #include "header.h" // main.cpp #include "header.h" #include "header2.h" int main() {}
链接器报错有多个func的定义:
报错
之前我们是用比较傻的解决方法,在标头周围放置一个包含保护来解决此问题:
// header.h #ifndef FUNC_H #define FUNC_H void func(){} #endif
但是,在带有模块中包含有相同符号是几乎不可能滴!
C++20模块的用法
为了照顾部分还没有接触过C++20的朋友,特意新增加了模块的用法部分。那么我们要怎么创建一个 Module 呢?20中引入了新的关键字 import、module,并使用保留关键字 export 来导入、定义和导出 Module,具体示例如下:
// hello_world.cpp export module demos.hello.world; export auto get_start() { return "Hello C++ Modules!"; } // main.cpp import demos.hello.world; import <iostream>; int main() { std::cout << get_start() << std::endl; }
以上就是一个 C++20 Modules版的Hello World,方便部分读者调试扩展。本文的目的是通过C++11标准的语法来暴露问题,来充分体现出C++20的优势,这样方能体现C++标准迭代的良好延续性,而非一上来就介绍C++20的语法糖,这部分需要读者自行了解。
模块优势总结
以上我用最简单的代码带你深入浅出的理解了C++11存在的问题,在20中模块都给出了解决方案。最后那就做个模块优点的总结归纳:
模块仅导入一次,不会造成重复编译输出。
导入模块的顺序没有区别。
模块中避免出现相同符号。
模块的代码逻辑结构更清晰。
由于使用了模块,因此无需将源代码分为接口和实现部分。
本页共130段,3864个字符,7666 Byte(字节)